﻿using System;
using System.Collections;
using System.Diagnostics;
using System.Management;
using System.Collections.Specialized;
using Microsoft.Win32;
using CMW_logic_testing_app.Properties;

namespace ParanoidMike.CacheMyWork
{
	/// <summary>
	/// The core business logic that analyzes all processes running on the system to determine which are visible user processes.
	/// </summary>
	class ProcessesAnalyzer
	{
		#region Variables

		// TODO: Consider changing this variable from a static variable to one that will always die once its local function caller dies
		internal static System.Diagnostics.Process process;

		/// <summary>
		/// A global Hashtable of the process objects found on the system, from which the calling application can select those that can be cached by the user.
		/// </summary>
		/// <remarks>
		/// This Hashtable is instantiated once and endures for the lifetime of this application.
		/// 
		/// There are occasions when two or more processes with the same name will be stored in this Hashtable; in such cases, the 'key' used to index the entries will differ by a numbered suffix for the 2nd and subsequent entries.
		/// </remarks>
		internal static System.Collections.Hashtable enumeratedProcesses = new Hashtable();

		#endregion

		#region Public Methods

		/// <summary>
		/// Evaluates all running processes and culls down a list of those that are cacheable user processes.
		/// </summary>
		/// <returns>
		/// Collection of processes started by, and visible to, the end user.
		/// </returns>
		/// <remarks>
		/// The most important outcome of this function is to ensure the user doesn't get presented with apps that don't appear in the Windows Taskbar.
		/// </remarks>
		public static StringDictionary GetCacheableUserProcesses()
		{
			StringDictionary userProcesses = new StringDictionary();

			// Instantiate the global enumeratedProcesses collection:
			///TODO: 2010-04-03 This initialization probably doesn't need to be here, with the same init in the class constructor.
			//enumeratedProcesses = new System.Collections.Hashtable();

			// First create the collection of process structures:
			BuildProcessesCollection();

			// TODO: !!! Figure out how to get a "dynamic" collection object that is the "current subset of enumeratedProcesses members that have .IsVisible = True", and pass in that Collection instead of enumeratedProcesses
			// Remove all processes that do not have any Windows (visible or invisible):
			HideProcessesWithoutWindows(enumeratedProcesses);

			// Remove all processes that do not create a visible Window:
				//TODO: 2010-04-03 No user setting should be called outside the UserRegistryConfiguration class - this should be re-factored into a global variable that's instantiated in the URC class
			if (Settings.Default.DisplayNonvisibleWindows == false)
			{
				//TODO: investigate why this call removes all remaining processes, so that I can re-enable it
				// HideProcessesWithoutVisibleWindows(enumeratedProcesses)
			}

			// TODO: implement some sort of LogWriter that passes its results back up to the calling code
			//LogFile.WriteLine("========= Finished removing processes that do not have a Window @ " + Date.Now)

			// Remove the well-known processes that the user (or the default Settings) have decided should never be cached by RunOnce:
			HideProcessesSelectedByUser(enumeratedProcesses, UserRegistryConfiguration.hiddenApps);

			// Remove processes that have already been configured to start in the user's context by various means
			HideProcessesAlreadyAutoStarted(enumeratedProcesses);

			// Remove those processes that were not started by the end user, since the user won't be able to (or won't want to) restart such processes next time anyway:
			// TODO: Determine if there's any difference between the results with and without this "not started by the user" function.
			HideProcessesNotStartedByCurrentUser(enumeratedProcesses);

			// Helping me to debug how well/poorly the function is behaving
			ArrayList procs = new ArrayList();
			procs.AddRange(enumeratedProcesses.Keys);
			procs.Sort();
			foreach (object proc in procs) { Console.WriteLine(proc.ToString()); }

			Console.WriteLine("Finished process enum...");

			// Finally, return the user processes collection
			foreach (EnumeratedProcess userProc in enumeratedProcesses.Values)
			{
				if (userProc.IsCacheable == true)
				{
					try
					{
						// NOTE: not using the iterated key from the EnumeratedProcesses collection, but re-creating a new key from the process name.  
						// Duplicates could be a problem, as we'll end up retrieving the first of possibly many process instances from EnumeratedProcesses - not necessarily the same one the user selected.
						userProcesses.Add(userProc.ProcessName, userProc.ProcessDescriptiveName);
					}
					catch (ArgumentException ex)
					{
						if (ex.Message.StartsWith("Item has already been added. Key in dictionary")) // the process being added is a duplicate - just discard it.
						{
							continue;
						}
						
					}
				}
			}
			return userProcesses;

			// TODO: implement some sort of LogWriter that passes its results back up to the calling code
			//LogFile.WriteLine("========= Finished removing processes *not* started by Me @ " + Date.Now)


			// TODO: implement some sort of LogWriter that passes its results back up to the calling code
			//LogFile.WriteLine("========= Finished creating final collection of processes for display @ " + Date.Now)


			// TODO: v2 - check whether any of the enumeratedProcesses are child processes of any other processes in the collection.  If so, Remove the child process.
			// (e.g. dexplore.exe is a child of devenv.exe, and can't typically be started on its own.)

			// TODO: v2 - Enumerate all documents held open by the resulting list of Apps, and cache them ahead of time so that the Tool can display them anytime the user selects the App name in the CheckedListBox.
			// e.g. use process.startinfo.arguments for some apps

			// TODO: v3 - Look for ways to return to the user's "place" in cached documents (e.g. use bookmarks in Word)
		}

		/// <summary>
		/// Constructor to initialize class-wide variables
		/// </summary>
		/// <remarks></remarks>
		static ProcessesAnalyzer()
		{
			UserRegistryConfiguration.GetAppConfig();
		}

		/// <summary>
		/// Takes the user's selections from the UI checklist and post-processes them to determine which processes to cache.
		/// </summary>
		/// <param name="selectedItems">
		/// Collection of the items as ProcessDescriptiveName that were selected by the user.
		/// </param>
		/// <remarks></remarks>
		public void SetUserSelections(StringCollection selectedItems)
		{
			//NOTE: these comments are from the original WinForms app code
			// First convert each selected item in the UIForm's CheckedListBox into a process object:
			// Retrieve the process from enumeratedProcesses collection whose index Key = CheckedItem.ToString, and enter its Command Line as a new entry in the RunOnce key
			foreach (string item in selectedItems)
			{
				EnumeratedProcess process = null;

				process = (EnumeratedProcess)enumeratedProcesses[item]; // Lookup EnumeratedProcess object, given the ProcessName index value

				// Then write each app to the HKCU RunOnce registry key as a separate entry, using the app's current command line
				//My.Computer.Registry.SetValue(HKCURUNONCE, _CheckedItem, _RegistryValue)
				UserRegistryConfiguration.SetRunOnceValue(process.ProcessName, process.ProcessCommandLine);
			}
		}

		#endregion

		#region Private Methods
		
		/// <summary>
		/// Enumerate all processes and build the collections of ProcessApplication objects: enumeratedProcesses, processLookupTable.
		/// </summary>
		/// <remarks></remarks>
		private static void BuildProcessesCollection()
		{
			// 2010-09-04 Once I figure out why this line isn't being executed by the Constructor, I can remove it from here and re-enable it in the Constructor
			enumeratedProcesses = new Hashtable();
			UserRegistryConfiguration.GetAppConfig();
			
			//TODO: PRI1 2010-04-04: remove all unnecessary processing from this loop - i.e. hiding processes [setting Properties] that are hidden later by subsequently-called functions
			foreach (var thisProcess in Process.GetProcesses())
			{
				// Note: the HashTable Collection's "Key" is created from process' name, so that I can index back into the Collection with just the ProcessName string.
				// TODO: !!! figure out a more unique index key than ProcessName, since there are many applications that can run more than one instance at a time.

				EnumeratedProcess currentProcess = null; // ??? 2010-09-04 Doesn't this line become redundant if we re-initialize at the next line?
				currentProcess = new EnumeratedProcess(); // Re-initialize this variable every pass through the For Each loop

				// TODO: PRI1 2010-04-04 - I believe there's an unhandled exception within this try...catch block - when a process exits before the EnumeratedProcess object has been added to the enumeratedProcesses hashtable.  See whether the catch (ApplicationException) is actually catching these occurrences or not.  May require setting breakpoint on one of the inner functions, killing the process it's currently processing, and seeing whether the exception gets handled correctly.
				try
				{
					// First make sure you're not doing needless work on the System or Idle "process", which aren't real processes
					if (thisProcess.ProcessName == "System" || thisProcess.ProcessName == "Idle")
					//if (thisProcess.ProcessName != "OUTLOOK" && thisProcess.ProcessName != "iexplore") // Useful for debugging
					{
						//ORIGINAL LINE: Exit Try
						// DEBUG NOTE: 2010-04-03 with the C# "continue" directive (substituted for "Exit Try" from the converted VB), I intend to simply skip to the next process in the for loop, not to stop processing the entire For loop
						continue;
					}

					// Create the EnumeratedProcess object to store all needed information about each process
					// By enumerating all necessary information up front, CacheMyWork and the user will be able to operate independently of whether the process has subsequently exited

					// ****************************************************************
					// Here's where we set all the Properties of the EnumeratedProcess
					// ****************************************************************

					// TODO: think again about whether it's necessary to check for the presence of Windows, when we're also checking for the presence of Visible Windows.  Now that CacheMyWork isn't removing entries from a collection, but just tagging them with all metadata, this may not only be redundant but anti-performance.
					if (DoesProcessHaveWindows(thisProcess)) // Checking whether any Windows (visible or not) are available
					{
						currentProcess.HasWindow = true;
					}

					// Set the IsHiddenByUser property according to whether the ProcessName was found in the user's Settings array at startup (when it was slurped into the hiddenApps Hashtable)
					currentProcess.IsHiddenByUser = UserRegistryConfiguration.hiddenApps.ContainsKey(thisProcess.ProcessName);

					try
					{
						// TODO: figure out how best to destroy the 'object' that encapsulates the IsWindowVisible API call, since it's exposed as a Shared Function - this is gonna be harder than I thought
						if (NativeMethods.IsWindowVisible(thisProcess.Handle))
						{
							currentProcess.IsWindowVisible = true;
						}

					}
					catch (System.ComponentModel.Win32Exception ex) // Only handle the "access is denied" COM exception for now
					{
						if (ex.NativeErrorCode == 5)
						{
							currentProcess.IsRemovedProgrammatically = true; // If there are any errors accessing a process this way, then we can safely filter out the process right now
						}
					}

					currentProcess.ProcessDescriptiveName = GetProcessDescriptiveName(thisProcess); // Retrieve the descriptive name for the process object

					currentProcess.ProcessName = thisProcess.ProcessName; // Retrieve the ProcessName for the process object

					// Retrieve the CommandLine for the current process object
					currentProcess.ProcessCommandLine = GetCommandLine(thisProcess);

					// Retrieve the UserName for the current process object, only if the user is likely (by now) to be presented this app
					if (currentProcess.IsCacheable != false)
					{
						currentProcess.ProcessOwner = GetProcessOwnerFromWmi(thisProcess);
					}

					//2010-09-04: commenting this out as it should no longer be needed; delete if no crashes due to this within a few days
					//// Finally, set the artificial state that will be used to subtract out processes one by one
					//currentProcess.IsCacheable = true;

					// Once the ProcessApplication object is created, add it to the enumeratedProcesses collection, which is the list that we'll be adjusting throughout this procedure.
					enumeratedProcesses.Add(currentProcess.ProcessName, currentProcess);

					
					// TODO: dump this lookup table as soon I'm able
					// TODO: !!! Is there some way to create a HashTable with *two* indices, not just one?  Then I could index into enumeratedProcesses with ProcessName or DescriptiveName
					// HACK: Create a lookup table to be able to lookup ProcessName by Descriptive Name, since I don't have a clear idea how to do this in a Collection of Structures
					//processLookupTable.Add(currentProcess.ProcessDescriptiveName, currentProcess.ProcessName)

					// TODO: what to do when I encounter two or more instances of IEXPLORE?  Just discard the 2nd and subsequent 
				}
				catch (System.ArgumentException ex)
				{
					// There are at least two case where this Exception occurs:
					//   (1) when there are two or more Processes of the same name running at the same time (e.g. svchost.exe) = "Add failed.  Duplicate key values supplied."
					//   (2) ex.Message = "Illegal characters in path." (e.g. csrss.exe, winlogon.exe)
					// Where (2) is the cause, just catch this exception and move on - the assumption being, the important apps to cache for next time only run one at a time.

					// TODO: implement some sort of LogWriter that passes its results back up to the calling code
					//LogFile.WriteLine("Failed to fully evaluate process " + process.ProcessName + "because " + ex.Message + " @ " + Date.Now)

					// In cases where there are two or more processes with the same name, create a unique value for the Hashtable entry to be added
					if (ex.Message.StartsWith("Item has already been added"))
					{
						// If it's Internet Explorer, then just discard this second process and move on.  See http://www.codeplex.com/CacheMyWork/Wiki/View.aspx?title=IEXPLORE&referringTitle=Documentation for detailed explanation of why.
						if (currentProcess.ProcessName == "IEXPLORE")
						{
							//ORIGINAL LINE: Exit Try
							// DEBUG NOTE: 2010-04-03 with the C# "continue" directive (substituted for "Exit Try" from the converted VB), I intend to simply skip to the next process in the for loop, not to stop the entire for loop processing
							continue;
						}

						// Append an integer (from 1 to infinity, theoretically) to the ProcessName and try to find that Key in the Hashtable
						// When that Key cannot be found in the Hashtable, use that Key to add a new instance of the process to the Hashtable
						int count = 0;
						string key = null;
						count = 0;
						do
						{
							count = count + 1;
							key = currentProcess.ProcessName + count.ToString();
						} while ( ! (enumeratedProcesses.ContainsKey(key) == false));

						enumeratedProcesses.Add(key, currentProcess);
					}
				}
				catch (System.ApplicationException)
				{
					// The only known case where this Exception occurs is when the process exits after it's added to the enumeratedProcesses array
					// Since we're tracking this state in the enumeratedProcesses class, we shall toggle the .Exists property to "False"
					currentProcess.Exists = false;
				}
				catch (System.InvalidOperationException)
				{
					// This occurs when the process currently being investigated has exited (thrown explicitly by Processes.GetProcessDescriptiveName())
					// Just best to let it go - when the process has exited, there's no use in trying to add it to the enumeratedProcesses collection
				}

				// TODO: dispose/finalize/null the currentProcess object?  Would that have any benefit (performance, memory)?
		   }
		}

		/// <summary>
		/// Determines if the process has any window objects as far as the operating system is concerned.
		/// </summary>
		/// <returns>True if process has at least one window (visible or not); False if no windows.</returns>
		/// <remarks></remarks>
		private static bool DoesProcessHaveWindows(Process process)
		{
			// Validate input before using
			if (process == null)
			{
				throw new ArgumentNullException("process");
			}
			try
			{
				//MessageBox.Show("process: " + process.ProcessName + vbCrLf + "MWH: " + process.MainWindowHandle.ToString)
				if (process.MainWindowHandle.ToInt32() != 0)
				{
					return true;
				}
				else
				{
					return false; // Added this line to eliminate the warning "Function doesn't return a value on all code paths."
				}
			}
			catch (System.ComponentModel.Win32Exception ex)
			{
				// e.g. "Access Denied" error when process.MainWindowHandle fails because the user doesn't have sufficient privilege
				// to the processes they don't own.  This also fails on Vista when user is Protected Admin - only elevated Admin can get past this.
				// TODO: figure out if I could re-order the functions so that this occurs *after* the RemoveProcessesNotStartedByMe() call, so that this function never tries to open a handle to a "foreign" process.
				return false; // Added this line to eliminate the warning "Function doesn't return a value on all code paths."
			}
		}

		/// <summary>
		/// Returns the active command line currently used to instantiate the current process. 
		/// </summary>
		/// <param name="process"></param>
		/// <returns></returns>
		private static string GetCommandLine(Process process)
		{
			string commandLine = string.Empty; // initialize local variable to avoid compiler warnings about "not all code paths..."

			try
			{
				// TODO: Re-examine the decision to gather the Command Line here up-front, as it's causing potentially many occasions of NullReferenceException for processes that do not have a known Command Line

				// NOTE: It appears that requesting the Command Line via WMI ends up with a string that has a trailing space character at the end of the string (i.e. after the second " delimiter).
				// This trailing space (for some unknown reason) causes Windows to fail to execute the Command Lines retrieved from RunOnce entries, so the trailing space must be deleted.
				// According to MSDN documentation for .TrimEnd(), a null parameter will trim whitespace (aka space characters) only.
				//commandLine = GetProcessPropertyFromWmi(ref process, "CommandLine").TrimEnd();
				string cmdLineTemp = string.Empty;
				cmdLineTemp = GetProcessPropertyFromWmi(ref process, "CommandLine");
				commandLine = cmdLineTemp.TrimEnd();
			}
			catch (System.ArgumentException)
			{
				// There is one known case where this Exception occurs:
				//   (1) when the process (e.g. System) does not have a command line

				// Rethrow this exception up the stack, so it can be handled more consistently - this immediately exits the Try...End Try routine
				throw;
			}
			return commandLine;
		}

		/// <summary>
		/// Returns a descriptive name that the end user will typically know as the "name" of the application/process.
		/// </summary>
		/// <param name="processShortName"></param>
		/// <returns></returns>
		/// <remarks>Not yet implemented.</remarks>
		private static string GetProcessDescriptiveName(string processShortName)
		{
			// Gets the process' process object and then calls into GetProcessDescriptiveName(proc as process)

			string processDescriptiveName = null;

			// Validate input on this non-private method
			if (processShortName == null)
			{
				throw new ArgumentNullException("processShortName");
			}

			try
			{
				// TODO: Code here
				// HACK: set to a value just to eliminate the warning
				processDescriptiveName = processShortName;
				// Call GetProcessDescriptiveName(proc as process)
			}
			catch (Exception)
			{
				// Catch 
				throw;
			}

			// TODO: Code here

			return processDescriptiveName;
		}

		/// <summary>
		/// Returns a descriptive name that the end user will typically know as the "name" of the application/process.
		/// </summary>
		/// <param name="process"></param>
		/// <returns></returns>
		private static string GetProcessDescriptiveName(Process process)
		{
			// HACK: I don't know why this needs to be Shared - I did that to resolve an Error that Visual Studio gave me.

			string processDescriptiveName = string.Empty; // Setting it to String.Empty to avoid VS error "variable is used before it has been assigned a value"
			string validatedProcessDescriptiveName = string.Empty; // Setting it to String.Empty to avoid VS error "variable is used before it has been assigned a value"

			// Validate input on this non-private method
			if (process == null)
			{
				throw new ArgumentNullException("process");
			}

			try
			{
				// Try to generate a readable Descriptive Name that doesn't contain an empty string (as is not uncommon in some apps for some of these fields)
				// TODO: Determine why this loop is executing twice in a row on each process
				if (string.IsNullOrEmpty(process.MainModule.FileVersionInfo.FileDescription) == false)
				{
					processDescriptiveName = process.MainModule.FileVersionInfo.FileDescription;
				}
				else if (string.IsNullOrEmpty(process.MainWindowTitle) == false)
				{
					processDescriptiveName = process.MainWindowTitle;
				}
				else if (string.IsNullOrEmpty(process.MainModule.FileVersionInfo.InternalName) == false && string.IsNullOrEmpty(process.MainModule.FileVersionInfo.ProductName) == false)
				{
					processDescriptiveName = process.MainModule.FileVersionInfo.ProductName + " (" + process.MainModule.FileVersionInfo.InternalName + ")";
				}
				else if (string.IsNullOrEmpty(process.MainModule.FileVersionInfo.InternalName) == false)
				{
					processDescriptiveName = process.MainModule.FileVersionInfo.InternalName;
				}
				else
				{
					// This should always return a non-empty value
					processDescriptiveName = process.ProcessName;
				}

			}
			catch (System.ComponentModel.Win32Exception)
			{
				// When the Win32 exception is "Access is denied" (ErrorCode=-2147467259), then we'll have to try a less invasive Property:
				// TODO: decide what to do on Vista with elevated Processes (which cause the "Access is denied" error) - should they be scheduled for restart?  If so, is there any workaround for the RunOnce mitigation against elevated processes in Vista?

				// This conditional code assumes Substring > 5 is True for Windows Vista, Win7, Windows Server 2008 and future OS's, but false for XP & 2003
				if (System.Environment.OSVersion.Version.Major > 5)
				{
					processDescriptiveName = process.ProcessName + " (running elevated or in another context)";
				}
				else
				{
					processDescriptiveName = process.ProcessName + " (running in another context)";
				}
			}
			catch (System.InvalidOperationException ex)
			{
				// When a process has exited between the time it's first detected and its FileDescription is retrieved, that'll result in this edge case Exception.
				// Under such circumstances, the right thing to do would be to remove the process from the array, which is something the calling code must do.
				// Therefore, we will throw a custom Exception that the calling code can handle
				throw new InvalidOperationException("Application has Exited or is otherwise not currently available", ex);
			}
			catch (System.ArgumentException ex)
			{
				// This will catch the "Illegal characters in path" exception that results when csrss.exe or winlogon.exe is the module tested by process.MainModule.FileVersionInfo.*
				// For csrss & winlogon, this is because System.Diagnostics.process.MainModule.FileName returns "\??\C:\WINDOWS\system32\winlogon.exe".
				if (ex.Message.StartsWith("Illegal characters in path") & (process.ProcessName == "winlogon" || process.ProcessName == "csrss"))
				{
					return process.ProcessName;
				}
			}

			// Finally, strip out the unneeded or error-causing characters from the Descriptive Name
			try
			{
				validatedProcessDescriptiveName = RemoveUnneededCharactersFromProcessDescriptiveName(processDescriptiveName);
			}
			catch (ArgumentNullException ex)
			{
				Console.Write("Couldn't get a Descriptive Name for the process = " + process.ProcessName + Environment.NewLine + "Exception Message: " + ex.Message + Environment.NewLine + "ex.Inner: " + ex.InnerException.ToString());
				//MessageBox.Show("Couldn't get a Descriptive Name for the process = " + process.ProcessName + Environment.NewLine + "Exception Message: " + ex.Message + Environment.NewLine + "ex.Inner: " + ex.InnerException.ToString());
			}

			return validatedProcessDescriptiveName;
		}

		/// <summary>
		/// Returns the EnumeratedProcess.ProcessName property.
		/// </summary>
		/// <param name="processDescriptiveName"></param>
		/// <returns></returns>
		private static string GetProcessName(string processDescriptiveName)
		{
			if (String.IsNullOrEmpty(processDescriptiveName))
			{
				throw new System.ArgumentException("Argument is null or empty", "processDescriptiveName");
			}
			foreach (EnumeratedProcess _Process in enumeratedProcesses)
			{
				if (_Process.ProcessDescriptiveName == processDescriptiveName)
				{
					return _Process.ProcessName;
				}
			}

			// I don't know what other conditions could exist, but better safe than sorry and I'd hate to generate another unhandled exception.
			return string.Empty;
		}

		/// <summary>
		/// This function returns the DOMAIN\username that owns the passed-in process.
		/// </summary>
		/// <param name="process"></param>
		/// <returns>DOMAIN\username of the process owner.</returns>
		/// <remarks></remarks>
		private static string GetProcessOwnerFromWmi(Process process)
		{
			int processID = 0;
			string processUserName = string.Empty; // initialized to avoid a warning in Visual Studio
			string processUserDomain = string.Empty; // initialized to avoid a warning in Visual Studio

			// Validate that the proc parameter isn't null, since the function is public, so that this code is less likely to be "hacked"
			if (process == null)
			{
				throw new ArgumentNullException("process");
			}

			// Once validated, it's safe to instantiate processID
			processID = process.Id;

			// Select queryable WMI object for that process:
			ManagementObjectSearcher _Searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Process WHERE ProcessId = " + processID);

			// This Try...Finally block is meant to enable us to Dispose of the searcher object before all references to it are out of scope (CA2000)
			try
			{
				foreach (ManagementObject _queryObj in _Searcher.Get())
				{
					ManagementBaseObject _ReturnedElements = _queryObj.InvokeMethod("GetOwner", null, null);
					try
					{
						processUserName = _ReturnedElements["User"].ToString();
						processUserDomain = _ReturnedElements["Domain"].ToString();
					}
					catch (NullReferenceException) // This normally occurs when it's a system process we're checking
					{
						return "NTAUTHORITY\\LOCALSYSTEM"; // I hope this doesn't cause major problems later on...
					}
				}

				// If the process for some reason doesn't return an Owner value, then let's avoid returning just "\" to the calling function
				if (String.IsNullOrEmpty(processUserDomain) || String.IsNullOrEmpty(processUserName))
				{
					return string.Empty;
				}
				else
				{
					return processUserDomain + "\\" + processUserName;
				}

			}
			finally
			{
				_Searcher.Dispose();
			}
		}

		/// <summary>
		/// Generic function to query WMI for any ProcessAttribute property and return its value.
		/// </summary>
		/// <param name="process">
		/// The process object to be queried (as a process object).
		/// </param>
		/// <param name="processAttribute">
		/// The WMI object to be queried for that process (as a String).
		/// </param>
		/// <returns>The value of the process' Property, as a String.</returns>
		/// <remarks></remarks>
		private static string GetProcessPropertyFromWmi(ref Process process, string processAttribute)
		{
			// Validate that the proc parameter isn't null, since the function is not private, so that this code is less likely to be "hacked"
			if (process == null)
			{
				throw new ArgumentNullException("process");
			}
			if (processAttribute == null)
			{
				throw new ArgumentNullException("processAttribute");
			}

			string returnValue = string.Empty; // Explicitly setting returnValue to String.Empty because the IDE warns I may not always set a returnValue via the For Each loop
			int processID = 0;
			processID = process.Id;

			// Instantiate a queryable WMI object for the process
			// RESEARCH: is there a more elegant SELECT parameter than "*" to retrieve the intended process?  Even though all process IDs have to be unique, it still seems like a bad idea to "SELECT *" if there was a better way to do this such as "SELECT 1st" or "SELECT the" [if there are such parameters].
			ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Process WHERE ProcessId = " + processID);

			// This Try...Finally block is meant to enable us to Dispose of the searcher object before all references to it are out of scope (CA2000)
			try
			{
				// RESEARCH: is there a more efficient way to get the Command Line for a process than to have this loop through *all* Management.ManagementObjects for the process?  That's a lot of excess cycles I'd rather not waste.
				foreach (ManagementObject queryObj in searcher.Get())
				{
					returnValue = (string)queryObj[processAttribute];
				}

				// This should throw an exception if no CommandLine (or other processAttribute) is retrieved
				if  (String.IsNullOrEmpty(returnValue))
				{
					// This function fails to retrieve a Command Line when passed a reference to e.g. the System process, since the System process *has* no command line.
					// Since a process without a Command Line will never be cached anyway, throw an exception to be clear to the calling code.
					throw new ArgumentException("This argument has no command line", process.ToString());

					// I would normally add Exit Function here, but it shouldn't be necessary since this If...End If is the final statement in the function.
				}
				else
				{
					return returnValue;
				}
			}
			finally
			{
				searcher.Dispose();
			}
		}

		/// <summary>
		/// Extracts all entries from HKLM and HKCU CurrentVersion\Run keys in the Registry.
		/// </summary>
		/// <param name="hive"></param>
		/// <param name="key"></param>
		/// <returns>A collection of strings, each representing a command line that appears in the Registry.</returns>
		/// <remarks></remarks>
		private static ArrayList GetRunKeyCommandLines(Microsoft.Win32.RegistryKey hive, string key)
		{
			if (hive == null)
			{
				throw new ArgumentNullException("hive");
			}
			if (String.IsNullOrEmpty(key))
			{
				throw new ArgumentNullException("key");
			}

			// TODO: Performance task - in future version, chop out the various parts of the CommandLine that aren't the ProcessName
			ArrayList registryValues = new ArrayList();
			Microsoft.Win32.RegistryKey activeKey = null;

			activeKey = hive.OpenSubKey(key);

			if (activeKey != null && activeKey.ValueCount > 0)
			{
				string[] values = activeKey.GetValueNames();
				foreach (string value in values)
				{
					// TODO: There can be Registry entries whose data types are REG_EXPAND_SZ - where should those be expanded, if anyplace?   Can't do it here when the datatype for the variable is String.

					// Test for null because the Registry value's data may be empty - e.g. in cases where Sysinternals Autoruns was used to disable certain entries
					if (String.IsNullOrEmpty(value) != true)
					{
						registryValues.Add(activeKey.GetValue(value));
					}
				}
			}
			return registryValues;
		}

		/// <summary>
		/// Filters out those processes that are already being started at every Windows startup and/or user logon.
		/// </summary>
		/// <param name="processes">Shared Hashtable collection of processes to test and potentially filter.</param>
		/// <remarks></remarks>
		private static void HideProcessesAlreadyAutoStarted(Hashtable processes)
		{
			// Pulls in all values from HKLM Run & HKCU Run keys, determines if any of the candidate processes are being auto-started, and eliminates any matches.

			const string hklmRunKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
			const string hkcuRunKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
			Microsoft.Win32.RegistryKey currentUserHive = null;
			Microsoft.Win32.RegistryKey localMachineHive = null;
			ArrayList autoStartCommandLines = null;

			// DEBUG NOTE: 2010-04-03 Original conversion to C# gave us this line: currentUserHive = Microsoft.Win32.Registry.CurrentUser();
			currentUserHive = Microsoft.Win32.Registry.CurrentUser;
			localMachineHive = Microsoft.Win32.Registry.LocalMachine;

			// Validate input on this public method
			if (processes == null)
			{
				throw new ArgumentNullException("processes");
			}
			else if (processes.Count != 0)
			{
				autoStartCommandLines = new ArrayList();

				// Gathering all command lines from both the HKLM & HKCU Run keys into one collection will make it more efficient to compare the ProcessName to all Command Lines
				foreach (string commandLine in GetRunKeyCommandLines(localMachineHive, hklmRunKey))
				{
					autoStartCommandLines.Add(commandLine);
				}

				foreach (string commandLine in GetRunKeyCommandLines(currentUserHive, hkcuRunKey))
				{
					autoStartCommandLines.Add(commandLine);
				}

				// Now this will loop through each process passed in and see if its ProcessName appears in any of the Command Lines
				// TODO: investigate whether this routine will ever give us a false positive - i.e. whether the ProcessName will likely/ever occur in other non-process Name parts of the Command Line
				foreach (EnumeratedProcess process in processes.Values)
				{
					foreach (string commandLine in autoStartCommandLines)
					{
						// This conditional routine will remove the process from enumeratedProcesses if it is already being started via one of the enumerated Auto-Start locations

						// Note: here we're using the String.IndexOf() method rather than String.Contains() method to optimize the case-insensitive comparison, since process.ProcessName is often Uppercase, and the Command Line is mixed-case
						// This technique was suggested here: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=311490&SiteId=1
						int index = 0;
						index = commandLine.IndexOf(process.ProcessName, 0, StringComparison.OrdinalIgnoreCase);
						// index can be "-1" if the ProcessName string doesn't occur in commandLine, so test that index is at least = 0
						if (index >= 0)
						{

							// TODO: to address WorkItem 3435, here's where to implement some tagging of the EnumeratedProcess class objects to be displayed as Read-Only
							// This'll require doing something like adding a ReadOnly boolean entry to the EnumeratedProcess class.
							process.IsRemovedProgrammatically = true;
							break;
						}
					}
				}

				//' RESEARCH: would it be better to pull in the existing Registry values and make the comparison in memory [= memory bloat], or compare each process to the Registry values directly [= multiple Registry reads]?

				// TODO: use the Contains() method for the String class to determine if the process Name is contained within the Command Line of any of the auto-started apps.

			}

		}

		/// <summary>
		/// Filters out those processes that were not started by the user in whose context the current application is running.
		/// </summary>
		/// <param name="processes">Shared Hashtable collection of processes to test and potentially filter.</param>
		/// <remarks></remarks>
		private static void HideProcessesNotStartedByCurrentUser(Hashtable processes)
		{
			// Setup username comparison variables:
			string myUsername = null;
			string myDomainUsername = null;
			// DEBUG NOTE: 2010-04-03 This was converted from the My namespace so I may have guessed incorrectly in which object to use
			// 2010-09-04: This definitely doesn't match process.ProcessOwner - is that because this is Win7, or because it's simply a bad conversion?
			myUsername = Environment.UserName;
			myDomainUsername = Environment.UserDomainName + "\\" + Environment.UserName;

			// Validate input on this public method
			if (processes == null)
			{
				throw new ArgumentNullException("processes");
			}
			else if (processes.Count != 0)
			{
				// Enumerate all processes, then check whether each process was started by the current user:
				foreach (EnumeratedProcess process in processes.Values)
				{
					// Compare the process' UserName, stored as the .ProcessOwner property, to the user context for the current application.
					// If it's the same, then we assume the process was started by the logged-in user, and should be considered a candidate for auto-restart i.e. left in the collection:
					if (process.ProcessOwner != myUsername && process.ProcessOwner != myDomainUsername)
					{
						process.IsRemovedProgrammatically = true;
					}
				} 
			}
		}

		//2010-09-04: This function *is* being called at present
		//TODO: 2010-04-03: if it turns out that this overload isn't needed, in favour of the following method, then delete this one.
		/// <summary>
		/// Compares the collection of candidate processes to the array of apps that are configured to be hidden, and toggles the IsRemovedProgrammatically property if a match is found.
		/// </summary>
		/// <param name="processes">Shared Hashtable collection of processes to test and potentially filter.</param>
		/// <param name="appsNeverDisplayed"></param>
		/// <remarks></remarks>
		private static void HideProcessesSelectedByUser(Hashtable processes, Hashtable appsNeverDisplayed)
		{
			EnumeratedProcess process = null;

			// Validate input on this public method
			if (processes == null)
			{
				throw new ArgumentNullException("processes");
			}
			else if (appsNeverDisplayed == null)
			{
				throw new ArgumentNullException("appsNeverDisplayed");
			}
			else if (processes.Count != 0 && appsNeverDisplayed.Count != 0)
			{
				//For Each process As EnumeratedProcess In processes.Values
				//    For Each app As String In appsNeverDisplayed
				//        If app.Contains(process.ProcessName) Then
				//            process.IsRemovedProgrammatically = True
				//        End If
				//    Next app
				//Next process

				// Since processes is a HashTable, we can index directly to the process
				foreach (string app in appsNeverDisplayed.Values)
				{
					try
					{
						// DEBUG NOTE: 2010-04-03 - this didn't convert cleanly, so I may have munged up the retrieval from the hashtable
						process = (EnumeratedProcess)processes[app];
						process.IsRemovedProgrammatically = true;

					}
					catch (NullReferenceException)
					{
						// The app does not exist as a process in the processes HashTable - that's cool, just move on to the next one
					}
				}

			}

		}

		/// <summary>
		/// Compares the collection of candidate processes to the array of apps that are configured to be hidden, and toggles the IsRemovedProgrammatically property if a match is found.
		/// </summary>
		/// <param name="enumeratedProcesses"></param>
		/// <param name="hiddenApps"></param>
		private static void HideProcessesSelectedByUser(Hashtable enumeratedProcesses, StringCollection hiddenApps)
		{
			EnumeratedProcess process = null;

			// Validate input on this public method
			if (enumeratedProcesses == null)
			{
				throw new ArgumentNullException("enumeratedProcesses");
			}
			else if (hiddenApps == null)
			{
				throw new ArgumentNullException("hiddenApps");
			}
			else if (enumeratedProcesses.Count != 0 && hiddenApps.Count != 0)
			{

				// Since enumeratedProcesses is a HashTable, we can index directly to the process
				foreach (string app in hiddenApps)
				{
					try
					{
						// DEBUG NOTE: 2010-04-03 - this didn't convert cleanly, so I may have munged up the retrieval from the hashtable
						process = (EnumeratedProcess)enumeratedProcesses[app];
						process.IsRemovedProgrammatically = true;
					}
					catch (NullReferenceException)
					{
						// The app does not exist as a process in the enumeratedProcesses HashTable - that's cool, just move on to the next one
					}
				}
			}
		}

		/// <summary>
		/// Filters out those processes that do not have a Window that is visible to the end user (e.g. present in the [Alt-[Tab] set of icons).
		/// </summary>
		/// <param name="processes">Shared Hashtable collection of processes to test and potentially filter.</param>
		/// <remarks></remarks>
		private static void HideProcessesWithoutVisibleWindows(Hashtable processes)
		{
			// TODO: Research any managed code Properties that could be used include process.StartInfo.CreateNoWindow & P.S.WindowStyle

			// Validate input on this public method
			if (processes == null)
			{
				throw new ArgumentNullException("processes");
			}
			else if (processes.Count != 0)
			{
				// Remove those processes whose Window is not visible:
				foreach (EnumeratedProcess process in processes.Values)
				{
					if (process.IsWindowVisible == false)
					{
						process.IsRemovedProgrammatically = true;
					}
				}
			}
		}

		/// <summary>
		/// Filters out those processes that lack any windows.
		/// </summary>
		/// <param name="processes"></param>
		/// <remarks></remarks>
		private static void HideProcessesWithoutWindows(Hashtable processes)
		{

			// Validate input on this public method
			if (processes == null)
			{
				throw new ArgumentNullException("processes");
			}
			else if (processes.Count != 0)
			{
				foreach (EnumeratedProcess process in processes.Values)
				{
					if (process.HasWindow == false)
					{
						process.IsRemovedProgrammatically = true;
					}
				}
			}
		}

		/// <summary>
		/// Removes characters from the process' DescriptiveName that may cause string manipulation errors elsewhere.
		/// </summary>
		/// <param name="processDescriptiveName"></param>
		/// <returns></returns>
		/// <remarks>
		/// The purpose here is to ensure that all String manipulation including populating and reading the Registry 
		/// won't puke on any illegal characters, e.g. +, :, (, ).  These characters are present in the Descriptive
		/// Name for Notepad++, for example.
		/// </remarks>
		private static string RemoveUnneededCharactersFromProcessDescriptiveName(object processDescriptiveName)
		{
			// The only reason this function is Shared is to resolve an error in its use by GetProcessDescriptiveName()

			string processDescriptiveNameRaw = null;
			string processDescriptiveNameFiltered = null;

			// Validate input on this non-private method
			if (processDescriptiveName == null)
			{
				throw new ArgumentNullException("processDescriptiveName");
			}

			processDescriptiveNameRaw = (string)processDescriptiveName;

			// TODO: add a function here that strips all characters out of the Descriptive Name that aren't necessary
			processDescriptiveNameFiltered = processDescriptiveNameRaw;

			return processDescriptiveNameFiltered;
		}

		#endregion

		#region Properties

		// HACK: Create a second collection to be able to look up a process by the displayed string in the UIForm checkedlistbox
		// TODO: !!! implement DataMember connections from the relevant collections to the UI
		// DEBUG NOTE: 2010-04-03 - converted to ArrayList from VB "Collection" object, which may not be working
		/// <summary>
		/// ArrayList that provides a descriptive name for each process as well as the enumeratedProcesses hashtable key.
		/// 
		/// This can be used to show the user the friendly names of the apps they're choosing among, and all the UI needs to return to the 
		/// class is the hashtable key.
		/// </summary>
		internal static System.Collections.ArrayList processLookupTable = new ArrayList();


		#endregion

	}
}
